﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Raona.Code.Contracts.Generation;
using Raona.Code.Contracts.EventArguments;
using Raona.Code.Model;
using WcfClientGenerator.Model.Config;
using WcfClientGenerator.Model.Common;
using System.Threading.Tasks;
using System.Threading;
using WcfClientTemplates.Templates;
using System.Collections.Concurrent;
using System.ComponentModel.Composition;
using System.IO;
using Raona.Util.Log;

namespace WcfClientTemplates.Generation
{
    [Export(typeof(ITemplateGenerator))]
    public class WcfClientTemplateGenerator : ITemplateGenerator
    {
        #region Constants

        private const string LOG_FILE_NAME = "WcfClientTemplateGenerator-log.txt";
        #endregion

        #region Event Publication

        /// <summary>
        /// Signals the completion of a step during the generation process
        /// </summary>
        public event EventHandler<ProgressEventArgs> ProgressChanged;
        /// <summary>
        /// Signals the completion of the generation process
        /// </summary>
        public event EventHandler<GenerationResultEventArgs> ProcessFinished;

        #endregion

        #region Instance Fields (Private)

        private List<Namespace> namespaces = null;
        private int stepCount = 0;
        private WcfClientConfiguration configuration = null;
        private CancellationTokenSource cancellationTokenSource = null;
        // project name -> collection of files to include in the project
        private Dictionary<string, ConcurrentBag<GeneratedFile>> generatedFiles = null;

        #endregion

        #region Instance Constructors (Public)

        public WcfClientTemplateGenerator()
        {
        }

        #endregion

        #region Instance Properties (Public)

        /// <summary>
        /// The configuration parameters needed during the generation process
        /// </summary>
        public string Configuration
        {
            set
            {
                this.configuration = (WcfClientConfiguration)SerializationManager.XmlDeserializeString(value, typeof(WcfClientConfiguration));
                if (this.configuration != null)
                {
                    // Each project will have generated a collection of files 
                    this.generatedFiles = new Dictionary<string, ConcurrentBag<GeneratedFile>>();
                    configuration.AsyncDestinationProjects.ForEach(project =>
                    {
                        this.generatedFiles.Add(project, new ConcurrentBag<GeneratedFile>());
                    });
                    configuration.SyncDestinationProjects.ForEach(project =>
                    {
                        this.generatedFiles.Add(project, new ConcurrentBag<GeneratedFile>());
                    });

                    FileLogger.LogFilePath = Path.Combine(configuration.GenerationFolder, LOG_FILE_NAME);
                }
            }
        }

        /// <summary>
        /// The interfaces needed during the generation process
        /// </summary>
        public IEnumerable<Namespace> Namespaces
        {
            set
            {
                this.namespaces = value.ToList();
                if (this.namespaces != null && this.namespaces.Count > 0)
                {
                    // (async contracts + async proxies + sync contracts + sync proxies) (begin / end)
                    this.stepCount = 4 * 2;
                }
                else
                    this.stepCount = 0;
            }
        }

        /// <summary>
        /// The number of steps needed to complete the process
        /// </summary>
        public int StepCount
        {
            get
            {
                return this.stepCount;
            }
        }

        #endregion

        #region Instance Methods (Public)

        /// <summary>
        /// Runs a generation process synchronously
        /// </summary>
        /// <returns></returns>
        public Dictionary<string, List<GeneratedFile>> Generate()
        {
            Dictionary<string, List<GeneratedFile>> results = null;

            if (this.configuration != null && this.namespaces != null && this.namespaces.Count() > 0)
            {
                // Create generation folder if necessary
                if (!Directory.Exists(this.configuration.GenerationFolder))
                    Directory.CreateDirectory(this.configuration.GenerationFolder);

                this.GenerateAsyncContracts(this.GenerateFileSimple);
                this.GenerateAsyncProxies(this.GenerateFileSimple);
                this.GenerateSyncContracts(this.GenerateFileSimple);
                this.GenerateSyncProxies(this.GenerateFileSimple);
                this.GenerateAsyncTaskProxies(this.GenerateFileSimple);

                results = WcfClientTemplateGenerator.GetResults(this.generatedFiles);
            }

            return results;
        }

        /// <summary>
        /// Starts the generation process asynchronously
        /// </summary>
        public void GenerateAsync()
        {
            try
            {
                if (this.configuration != null && this.namespaces != null && this.namespaces.Count() > 0)
                {
                    this.StartGenerationTasks();
                }
            }
            catch (OperationCanceledException operationCancelledException)
            {
                FileLogger.Log("GenerateAsync cancelled ({0})", operationCancelledException.Message);
                this.OnProcessCompleted(null, operationCancelledException);
            }
            catch (Exception exception)
            {
                FileLogger.Log("GenerateAsync exception ({0})", exception.Message);
                this.OnProcessCompleted(null, exception);
            }
        }

        /// <summary>
        /// Cancels the generation process
        /// </summary>
        public void Cancel()
        {
            if (this.cancellationTokenSource != null)
            {
                this.cancellationTokenSource.Cancel();
            }
        }

        #endregion

        #region Instance Methods (Private)

        /// <summary>
        /// Starts the process tasks
        /// </summary>
        /// <param name="cancellationToken"></param>
        private void StartGenerationTasks()
        {
            try
            {
                // Create a cancellation token
                if (this.cancellationTokenSource != null)
                    this.cancellationTokenSource.Dispose();
                this.cancellationTokenSource = new CancellationTokenSource();

                // Create generation folder if necessary
                if (!Directory.Exists(this.configuration.GenerationFolder))
                    Directory.CreateDirectory(this.configuration.GenerationFolder);

                // Start generation process
                Task.Factory.StartNew(() =>
                {
                    try
                    {
                        // Create a task for each file that must be generated
                        Task[] tasks = this.CreateGenerationTasks();

                        // Wait for the generation of all files
                        Task.WaitAll(tasks, this.cancellationTokenSource.Token);

                        // Signal the process is finished
                        this.OnProcessCompleted(this.generatedFiles, null);
                    }
                    catch (Exception exception)
                    {
                        FileLogger.Log("StartGenerationTasks exception ({0})", exception.ToString());
                        this.OnProcessCompleted(null, exception);
                    }

                });
            }
            catch (Exception exception)
            {
                FileLogger.Log("StartGenerationTasks exception ({0})", exception.Message);
                this.OnProcessCompleted(null, exception);
            }
        }

        /// <summary>
        /// Creates and starts a task for each file that must be generated
        /// </summary>
        /// <returns></returns>
        private Task[] CreateGenerationTasks()
        {
            Action[] actions = new Action[] {
                            () => this.GenerateAsyncContracts(this.GenerateFile),
                            () => this.GenerateAsyncProxies(this.GenerateFile),
                            () => this.GenerateSyncContracts(this.GenerateFile),
                            () => this.GenerateSyncProxies(this.GenerateFile),
                            () => this.GenerateAsyncTaskProxies(this.GenerateFile)
                        };

            Task[] tasks = new Task[actions.Length];
            for (int i = 0; i < tasks.Length; i++)
            {
                tasks[i] = Task.Factory.StartNew(actions[i], this.cancellationTokenSource != null ? this.cancellationTokenSource.Token : CancellationToken.None);
            }
            return tasks;
        }

        private void GenerateFile(Func<int, string> namespaceTextFunc, Func<string> usingsTextFunc, string fileName, List<string> projects)
        {
            FileLogger.Log("Starting {0} generation.", fileName);

            // Do work only if the target project list is not empty
            // and there are namespaces to generate code for
            int namespaceCount = this.namespaces.Count();
            if (projects.Count > 0 && namespaceCount > 0)
            {
#if (DEBUG)
                System.Diagnostics.Debug.WriteLine("File: {0}. Chunk #: {1}", fileName, namespaceCount + 1);
#endif
                // Allocate an array to hold the generated file chunks
                string[] fileChunks = new string[namespaceCount + 1]; // usings + n namespaces

                // Generate interface code for each namespace
                Parallel.For(0, namespaceCount, new ParallelOptions { CancellationToken = this.cancellationTokenSource != null ? this.cancellationTokenSource.Token : CancellationToken.None },
                    i =>
                    {
                        string chunkText = namespaceTextFunc(i);
                        fileChunks[i + 1] = chunkText;
                    });

                // Generate the usings section
                string usingsText = usingsTextFunc();
                fileChunks[0] = usingsText;

                // Assemble file by writing its chunks to disk
                string path = Path.Combine(this.configuration.GenerationFolder, fileName);
                using (StreamWriter writer = new StreamWriter(path, false))
                {
                    for (int i = 0; i < namespaceCount + 1; i++)
                    {
                        writer.Write(fileChunks[i]);

                        if (this.cancellationTokenSource != null)
                            this.cancellationTokenSource.Token.ThrowIfCancellationRequested();
                    }
                    writer.Close();
                }
                GeneratedFile generatedFile = new GeneratedFile { FullPath = path, FileName = fileName };

                projects.ForEach(project =>
                {
                    this.generatedFiles[project].Add(generatedFile);
                });
            }

            FileLogger.Log("Ending {0} generation.", fileName);
        }

        private void GenerateFileSimple(Func<int, string> namespaceTextFunc, Func<string> usingsTextFunc, string fileName, List<string> projects)
        {
            FileLogger.Log("Starting {0} generation.", fileName);

            // Do work only if the target project list is not empty
            // and there are namespaces to generate code for
            int namespaceCount = this.namespaces.Count();
            if (projects.Count > 0 && namespaceCount > 0)
            {
#if (DEBUG)
                System.Diagnostics.Debug.WriteLine("File: {0}. Chunk #: {1}", fileName, namespaceCount + 1);
#endif
                // Allocate an array to hold the generated file chunks
                string[] fileChunks = new string[namespaceCount + 1]; // usings + n namespaces

                // Generate interface code for each namespace
                for (int i = 0; i < namespaceCount; i++)
                {
                    string chunkText = namespaceTextFunc(i);
                    fileChunks[i + 1] = chunkText;
                }

                // Generate the usings section
                string usingsText = usingsTextFunc();
                fileChunks[0] = usingsText;

                // Assemble file by writing its chunks to disk
                string path = Path.Combine(this.configuration.GenerationFolder, fileName);
                using (StreamWriter writer = new StreamWriter(path, false))
                {
                    for (int i = 0; i < namespaceCount + 1; i++)
                    {
                        writer.Write(fileChunks[i]);

                        if (this.cancellationTokenSource != null)
                            this.cancellationTokenSource.Token.ThrowIfCancellationRequested();
                    }
                    writer.Close();
                }
                GeneratedFile generatedFile = new GeneratedFile { FullPath = path, FileName = fileName };

                projects.ForEach(project =>
                {
                    this.generatedFiles[project].Add(generatedFile);
                });
            }

            FileLogger.Log("Ending {0} generation.", fileName);
        }

        private void GenerateAsyncContracts(
            Action<Func<int, string>, Func<string>, string, List<string>> generateFile)
        {
            FileLogger.Log("Generating Async Contracts.");
            this.OnProgress("Generating Async Contracts.");

            generateFile(
                (i) =>
                {
                    Interfaces interfaces = new Interfaces { Namespaces = new Namespace[] { this.namespaces[i] } };
                    string interfacesText = interfaces.TransformText();
                    return interfacesText;
                },
                () =>
                {
                    InterfacesUsings interfacesUsings = new InterfacesUsings { SpecificUsings = this.configuration.SpecificUsings };
                    string usingsText = interfacesUsings.TransformText();
                    return usingsText;
                },
                this.configuration.AsyncContractsFileName,
                this.configuration.AsyncDestinationProjects);

            FileLogger.Log("Generated Async Contracts.");
            this.OnProgress("Generated Async Contracts.");
        }

        private void GenerateAsyncProxies(
            Action<Func<int, string>, Func<string>, string, List<string>> generateFile)
        {
            FileLogger.Log("Generating Async Proxies.");
            this.OnProgress("Generating Async Proxies.");

            generateFile(
                (i) =>
                {
                    Proxies proxies = new Proxies { Namespaces = new Namespace[] { this.namespaces[i] } };
                    string proxiesText = proxies.TransformText();
                    return proxiesText;
                },
                () =>
                {
                    ProxiesUsings proxiesUsings = new ProxiesUsings { Namespaces = this.namespaces, SpecificUsings = this.configuration.SpecificUsings };
                    string usingsText = proxiesUsings.TransformText();
                    return usingsText;
                },
                this.configuration.AsyncProxiesFileName,
                this.configuration.AsyncDestinationProjects);

            FileLogger.Log("Generated Async Proxies.");
            this.OnProgress("Generated Async Proxies.");
        }

        private void GenerateSyncContracts(
            Action<Func<int, string>, Func<string>, string, List<string>> generateFile)
        {
            FileLogger.Log("Generating Sync Contracts.");
            this.OnProgress("Generating Sync Contracts.");

            generateFile(
                (i) =>
                {
                    SyncInterfaces interfaces = new SyncInterfaces { Namespaces = new Namespace[] { this.namespaces[i] } };
                    string interfacesText = interfaces.TransformText();
                    return interfacesText;
                },
                () =>
                {
                    InterfacesUsings interfacesUsings = new InterfacesUsings { SpecificUsings = this.configuration.SpecificUsings };
                    string usingsText = interfacesUsings.TransformText();
                    return usingsText;
                },
                this.configuration.SyncContractsFileName,
                this.configuration.SyncDestinationProjects);

            this.OnProgress("Generated Sync Contracts.");
            FileLogger.Log("Generated Sync Contracts.");
        }

        private void GenerateSyncProxies(
            Action<Func<int, string>, Func<string>, string, List<string>> generateFile)
        {
            FileLogger.Log("Generating Sync Proxies.");
            this.OnProgress("Generating Sync Proxies.");

            generateFile(
                (i) =>
                {
                    SyncProxies proxies = new SyncProxies { Namespaces = new Namespace[] { this.namespaces[i] } };
                    string proxiesText = proxies.TransformText();
                    return proxiesText;
                },
                () =>
                {
                    ProxiesUsings proxiesUsings = new ProxiesUsings { Namespaces = this.namespaces, SpecificUsings = this.configuration.SpecificUsings };
                    string usingsText = proxiesUsings.TransformText();
                    return usingsText;
                },
                this.configuration.SyncProxiesFileName,
                this.configuration.SyncDestinationProjects);

            FileLogger.Log("Generated Sync Proxies.");
            this.OnProgress("Generated Sync Proxies.");
        }

        private void GenerateAsyncTaskProxies(
            Action<Func<int, string>, Func<string>, string, List<string>> generateFile)
        {
            FileLogger.Log("Generating Async Task Proxies.");
            this.OnProgress("Generating Async Task Proxies.");

            generateFile(
                (i) =>
                {
                    AsyncTaskProxies proxies = new AsyncTaskProxies { Namespaces = new Namespace[] { this.namespaces[i] } };
                    string proxiesText = proxies.TransformText();
                    return proxiesText;
                },
                () =>
                {
                    AsyncTaskProxiesUsings proxiesUsings = new AsyncTaskProxiesUsings { Namespaces = this.namespaces, SpecificUsings = this.configuration.SpecificUsings };
                    string usingsText = proxiesUsings.TransformText();
                    return usingsText;
                },
                this.configuration.AsyncTaskProxiesFileName,
                this.configuration.AsyncDestinationProjects);

            FileLogger.Log("Generated Async Task Proxies.");
            this.OnProgress("Generated Async Task Proxies.");
        }

        private void OnProcessCompleted(Dictionary<string, ConcurrentBag<GeneratedFile>> generatedFiles, Exception error)
        {
            if (this.ProcessFinished != null)
            {
                Dictionary<string, List<GeneratedFile>> generatedFilesArgument = WcfClientTemplateGenerator.GetResults(generatedFiles);
                GenerationResultEventArgs eventArgs = new GenerationResultEventArgs { Error = error, GeneratedFiles = generatedFilesArgument };
                this.ProcessFinished(this, eventArgs);
            }
        }

        private static Dictionary<string, List<GeneratedFile>> GetResults(Dictionary<string, ConcurrentBag<GeneratedFile>> generatedFiles)
        {
            Dictionary<string, List<GeneratedFile>> generatedFilesArgument = null;
            if (generatedFiles != null)
            {
                generatedFilesArgument = new Dictionary<string, List<GeneratedFile>>();
                foreach (string project in generatedFiles.Keys)
                {
                    generatedFilesArgument.Add(project, new List<GeneratedFile>(generatedFiles[project]));
                }
            }
            return generatedFilesArgument;
        }

        private void OnProgress(string message)
        {
            if (this.ProgressChanged != null)
            {
                this.ProgressChanged(this, new ProgressEventArgs { Message = message });
            }
        }

        #endregion
    }
}
